// godownloader project godownloader.go
package godownloader

import (
	"context"
	"errors"
	"fmt"
	"io"
	"math"
	"net/http"
	"os"
	"regexp"
	"strconv"
	"time"

	"golang.org/x/time/rate"
)

type CallBacker func(d *Downloader)

const (
	DefaultLimitChunkedSize = 32 * 1024 // 32k for chunk download
)

var (
	rangeComile *regexp.Regexp

	ErrUrl             = errors.New("url parse failed")
	ErrHttpStatus      = errors.New("http status err")
	ErrHttpNoBody      = errors.New("http no body")
	ErrHttpRange       = errors.New("http content-range err")
	ErrFileSeek        = errors.New("file seek no match")
	ErrHttpFileChanged = errors.New("http file changed")
)

type Downloader struct {
	limiter          *rate.Limiter
	limitChunkedNum  int
	limitChunkedSize int `json: "chunkesize"`

	keyId int64  `json: "keyid"`
	etag  string `json: "etag"`

	readDev  io.ReadCloser
	writeDev io.WriteCloser

	limitCtx   context.Context
	cannelFunc context.CancelFunc
	sizecall   CallBacker
	endcall    CallBacker
	initcall   func(*Downloader) error

	nsize       int64 `json: "nsize"`        // already size
	contentSize int64 `json: "contenlength"` // all size

	reopen bool
	cannle bool
	finish bool `json: "success"`

	err error
}

func init() {
	rangeComile, _ = regexp.Compile("bytes	+(\\d+)-\\d+/(\\d+)$")
}

func NewHttpDownloadFile(rawurl string, headers map[string]string,
	outputfilename string) (*Downloader, error) {

	fd, err := os.OpenFile(outputfilename, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, os.ModePerm)
	if err != nil {
		return nil, err
	}

	return NewHttpDownloader(rawurl, headers, fd)
}

func NewHttpDownloader(rawurl string, headers map[string]string,
	wfd io.WriteCloser) (*Downloader, error) {

	cli := http.DefaultClient
	req, err := http.NewRequest("GET", rawurl, nil)
	if err != nil {
		return nil, err
	}

	cli.Timeout = 60 * time.Second

	for k, v := range headers {
		req.Header.Add(k, v)
	}

	d := new(Downloader)
	d.limitChunkedSize = DefaultLimitChunkedSize
	d.writeDev = wfd

	d.initcall = func(this *Downloader) error {
		if this.reopen && this.nsize > 0 {
			this.err = nil
			var rangeVal string
			if this.contentSize > 0 {
				rangeVal = fmt.Sprint("bytes=", this.nsize, "-", this.contentSize)
				req.Header.Add("Range", rangeVal)
			} else {
				this.nsize = 0
			}

			if fd, ok := d.writeDev.(*os.File); ok {
				this.writeDev, err = os.OpenFile(fd.Name(), os.O_CREATE|os.O_WRONLY, os.ModePerm)
				if err != nil {
					return errors.New("reopen failed:" + err.Error())
				}

				n, err := this.writeDev.(*os.File).Seek(this.nsize, os.SEEK_SET)
				if err != nil {
					return errors.New("reopen seek failed:" + err.Error())
				} else if n != this.nsize {
					return ErrFileSeek
				}
			}
		}

		resp, err := cli.Do(req)
		if err != nil {
			return err
		}

		if this.nsize != 0 {
			if resp.StatusCode != 206 {
				this.nsize = 0
				goto Next
			}

			respRange := resp.Header.Get("Content-Range")
			if respRange == "" {
				return ErrHttpRange
			}

			match := rangeComile.FindStringSubmatch(respRange)
			if match == nil {
				return ErrHttpRange
			}

			//throught regex
			rangeStart, err := strconv.ParseInt(match[1], 10, 64)
			rangeEnd, err := strconv.ParseInt(match[2], 10, 64)

			if err != nil || rangeStart != this.nsize {
				return ErrHttpRange
			}

			if this.contentSize != -1 && this.contentSize != rangeEnd {
				return ErrHttpFileChanged
			}

			etag := resp.Header.Get("etag")
			if etag != this.etag {
				return ErrHttpFileChanged
			}

		} else if resp.StatusCode != 200 {
			return ErrHttpStatus
		}

	Next:
		this.contentSize = resp.ContentLength
		this.etag = resp.Header.Get("etag")
		if this.contentSize == 0 {
			return ErrHttpNoBody
		} else if this.contentSize == -1 {
			this.nsize = 0
		}

		this.readDev = resp.Body

		return nil
	}

	return d, nil
}

func NewDownloader(r io.ReadCloser, w io.WriteCloser) (*Downloader, error) {
	d := new(Downloader)
	d.readDev = r
	d.writeDev = w
	d.limitChunkedSize = DefaultLimitChunkedSize
	return d, nil
}

func (d *Downloader) Run() error {
	var err error

	if d.initcall != nil {
		err = d.initcall(d)
		if err != nil {
			d.err = err
			goto failed
		}
	}

	if d.limitCtx == nil {
		ctx, cannel := context.WithCancel(context.TODO())

		d.cannelFunc = cannel
		d.limitCtx = ctx
	}

	for {
		count := d.limitChunkedNum

		for i := 0; i < count+1; i++ {
			if d.limiter != nil {
				err = d.limiter.Wait(d.limitCtx)
				if err != nil {
					d.err = err
					goto failed
				}
			}

			if d.cannle {
				goto failed
			}

			n, err := io.CopyN(d.writeDev, d.readDev, int64(d.limitChunkedSize))
			d.nsize += n
			if err != nil {
				if err != io.EOF {
					d.err = err
					goto failed
				}
				goto success
			}

			if d.sizecall != nil {
				d.sizecall(d)
			}
		}
	}

success:
	d.contentSize = d.nsize

failed:
	d.finish = true
	if d.endcall != nil {
		d.endcall(d)
	}

	if d.cannle {
		d.err = nil
	}

	d.readDev.Close()
	d.writeDev.Close()
	return d.err
}

func (d *Downloader) Pause() {
	d.cannle = true
	d.reopen = false

	if d.cannelFunc != nil {
		d.cannelFunc()
	}

	if d.readDev != nil {
		d.readDev.Close()
	}

	if d.writeDev != nil {
		d.writeDev.Close()
	}
}

func (d *Downloader) SetRepen() {
	d.reopen = true
	d.finish = false
	d.cannle = false
	d.err = nil
}

func (d *Downloader) SetWriter(w io.WriteCloser) {
	d.writeDev = w
}

func (d *Downloader) SetReader(r io.ReadCloser) {
	d.readDev = r
}

func (d *Downloader) SetLmitChunkedSize(size uint) {
	if size != 0 && size < math.MaxInt32 {
		d.limitChunkedSize = int(size)
	}
}

func (d *Downloader) SetLimiter(l *rate.Limiter) {
	if l != nil {
		d.limiter = l
		d.limitChunkedNum = l.Burst()
	}
}

//after set_limit_chunked_size
func (d *Downloader) SetLimitRate(size uint64) {
	n := size / uint64(d.limitChunkedSize)
	d.limiter = rate.NewLimiter(rate.Limit(n), int(n))
	d.limitChunkedNum = int(n)
}

func (d *Downloader) SetEndCall(f CallBacker) {
	d.endcall = f
}

func (d *Downloader) SetSizeCall(f CallBacker) {
	d.sizecall = f
}

func (d *Downloader) SetInitCall(f func(d *Downloader) error) {
	d.initcall = f
}

func (d *Downloader) GetAlreadySize() int64 {
	return d.nsize
}

func (d *Downloader) GetContentLength() int64 {
	return d.contentSize
}

func (d *Downloader) SetLengthAndASize(length, nsize int64) {
	d.contentSize = length
	d.nsize = nsize
}

func (d *Downloader) GetErr() error {
	return d.err
}

func (d *Downloader) IsFinish() bool {
	return d.finish
}
